"""
A graphics policy represents how visually a target will be chosen during one stage of targetting. One spell can utilize
multiple graphics policies because of the ability to use multiple stages of targetting.
"""


from CvPythonExtensions import *

gc = CyGlobalContext()
localtext = CyTranslator()

import GerikSpellsUtils
import GerikSpellsInterface

import math
import copy # For making clones of target infos

class GSBase:
    def __init__(self):
        self.m_pTargetInfoPolicy = None
        self.m_pTargetSelectCallback = None

        self.m_pTargetInfo = None
        
        self.m_pCacheAffectedList = None
        self.m_pCacheUnaffectedList = None

        self.m_szCursorType = "CURSOR_DEFAULT"

    def getCursorType(self):
        """
        Arguments:
            None

        Returns:
            The string representing the cursor type used for this graphics stage.
        """
        return self.m_szCursorType

    def setCursorType(self, szCursorType):
        """
        Arguments:
            szCursorType: The string represeting the type of cursor to use for this graphics stage.

        Returns:
            None
        """
        self.m_szCursorType = szCursorType

    def start(self, pTargetInfoPolicy, pTargetSelectCallback, pTargetList):
        self.m_pTargetInfoPolicy = pTargetInfoPolicy
        self.m_pTargetSelectCallback = pTargetSelectCallback
        self.m_pTargetInfo = copy.copy(self.m_pTargetInfoPolicy)

        # Sets the interfacemode and begins to show the screen
        GerikSpellsInterface.setLocalSpellCursor(self.getCursorType())

    def stop(self, bCancel):
        self.undraw()
        self.m_pTargetInfoPolicy = None
        self.m_pTargetSelectCallback = None
        self.m_pTargetInfo = None

    def updateTarget(self, pTarget):
        self.m_pTargetInfo.setTarget(pTarget)
        pCurrentSpell = GerikSpellsInterface.getCurrentSpell()
        if pCurrentSpell:
            pCastingGroup = pCurrentSpell.getCastingGroup()
            pCastingPlot = pCurrentSpell.getCastingPlot()

            pTargetLists = self.m_pTargetInfo.getSecondaryTargetList(pCastingGroup, pCastingPlot)
            self.m_pCacheAffectedList = pTargetLists[0]
            self.m_pCacheUnaffectedList = pTargetLists[1]

    def targetSelected(self, pTargetInfo):
        # Tells the next system to start.
        if self.m_pTargetSelectCallback:
            self.m_pTargetSelectCallback(pTargetInfo)


    def mouseOverPlot(self):
        pass

    def getBars(self):
        return self.m_pTargetInfo.getBars()

    def onUpdate(self):
        raise NotImplementedError


class GSPlot(GSBase):
    def __init__(self):
        """
        Arguments:
            None
        """
        GSBase.__init__(self)
        self.m_bShowRangeGraphics = False
        self.m_pRangePlotList = None
        
        self.m_szAffectedPlotColor = "COLOR_GREEN"
        self.m_fAffectedPlotAlpha = 1.0
        self.m_szUnaffectedPlotColor = "COLOR_WHITE"
        self.m_fUnaffectedPlotAlpha = 1.0
        self.m_szRangePlotColor = "COLOR_WHITE"
        self.m_fRangePlotAlpha = 0.4

        self.m_iAffectedPlotTexture = PlotStyles.PLOT_STYLE_BOX_OUTLINE
        self.m_iUnaffectedPlotTexture = PlotStyles.PLOT_STYLE_BOX_OUTLINE
        self.m_iRangePlotTexture = PlotStyles.PLOT_STYLE_TARGET

    # Range functions
    def getShowRangeGraphics(self):
        """
        Arguments:
            None
        Returns
            Whether or not to color the plots within range.

        The plots that are within range are determined by using the callback found in the TargetInfo.
        """
        return self.m_bShowRangeGraphics

    def setShowRangeGraphics(self, bShow):
        """
        Arguments:
            bShow: Whether or not to color the plots within range.
        Returns
            None

        The plots that are within range are determined by using the callback found in the TargetInfo.
        """
        self.m_bShowRangeGraphics = bShow        

    # Plot Colors & Alpha
    def getAffectedPlotColor(self):
        """
        Arguments:
            None
        Returns
            String that represents the plot color that an "affected" plot should be colored as.
        """
        return self.m_szAffectedPlotColor

    def getAffectedPlotAlpha(self):
        """
        Arguments:
            None
        Returns
            Float variable (between 0.0 - 1.0) that determines the alpha of the plot color.
            0.0 is invisible, 1.0 is fully visible.
        """
        return self.m_fAffectedPlotAlpha

    def setAffectedPlotColor(self, szColor, fAlpha):
        """
        Arguments:
            szColor: The string representing the color that the affected plots texture will be.
            fAlpha: The float (0.0-1.0) variable of the alpha value for the color.
                    0.0 is invisible, 1.0 is fully visible.
        Returns
            None
        """
        self.m_szAffectedPlotColor = szColor
        self.m_fAffectedPlotAlpha = fAlpha

    def getUnaffectedPlotColor(self):
        """
        Arguments:
            None
        Returns
            String that represents the plot color that an "unaffected" plot should be colored as.
        """
        return self.m_szUnaffectedPlotColor

    def getUnaffectedAlpha(self):
        """
        Arguments:
            None
        Returns
            Float variable (between 0.0 - 1.0) that determines the alpha of the color of affected plots.
            0.0 is invisible, 1.0 is fully visible.

        """
        return self.m_fUnaffectedPlotAlpha

    def setUnaffectedPlotColor(self, szColor, fAlpha):
        """
        Arguments:
            szColor: String that represents the color of the plot texture.
            fAlpha: floating value (0.0-1.0) that represents the alpha of the plot texture.
            0.0 is invisible, 1.0 is fully visible.
        Returns
            None

        """
        self.m_szUnaffectedPlotColor = szColor
        self.m_fUnaffectedPlotAlpha = fAlpha

    def getRangePlotColor(self):
        """
        Arguments:
            None
        Returns
            String that represents the plot color that an "unaffected" plot should be colored as.
        """
        return self.m_szRangePlotColor

    def getRangePlotAlpha(self):
        """
        Arguments:
            None
        Returns
            Float variable (between 0.0 - 1.0) that determines the alpha of the color of ranged plots.
            0.0 is invisible, 1.0 is fully drawn.
        """
        return self.m_szRangePlotAlpha

    def setRangePlotColor(self, szColor, fAlpha):
        """
        Arguments:
            szColor: The string representing the color that the ranged plots texture will be.
            fAlpha: The float (0.0-1.0) variable of the alpha value for the color.
                    0.0 is invisible, 1.0 is fully visible.
        Returns
            None
        """
        self.m_szRangePlotColor = szColor
        self.m_fRangePlotAlpha = fAlpha

    # Plot textures
    def getAffectedPlotTexture(self):
        """
        Arguments:
            None
        Returns
            The [PlotTextureID] of affected plots.
        """
        return self.m_iAffectedPlotTexture

    def setAffectedPlotTexture(self, iTexture):
        """
        Arguments:
            iTexture: The [PlotTextureID] of affected plots.
        Returns
            None

        """
        self.m_iAffectedPlotTexture = iTexture

    def getUnaffectedPlotTexture(self):
        """
        Arguments:
            None
        Returns
            The [PlotTextureID] of affected plots.
        """
        return self.m_iUnaffectedPlotTexture

    def setUnaffectedPlotTexture(self, iTexture):
        """
        Arguments:
            iTexture: The [PlotTextureID] of affected plots.
        Returns
            None
        """
        self.m_iUnaffectedPlotTexture = iTexture

    def getRangeGraphicsTextureID(self):
        """
        Arguments:
            None
        Returns
            The [PlotTextureID] of affected plots.
        """
        return self.m_iRangeGraphicsTextureID

    def setRangeGraphicsTextureID(self, iTextureID):
        """
        Arguments:
            iTexture: The [PlotTextureID] of affected plots.
        Returns
            None
        """
        self.m_iRangeGraphicsTextureID = iTextureID

    # The real juicy functions
    def start(self, pTargetInfoPolicy, pTargetSelectCallback, pTargetList): # pTargetList is range plot list
        """
        Arguments:
            pTargetInfoPolicy: The TargetInfo class that will be used to determine a target.
            pTargetSelectCallbcak: The function that will be called with the selected TargetInfo when selected.
            pTargetList: The list of available targets to choose from. For this policy, this list is a list of plots
                         that represent all the plots that are within range.
        Returns
            None
        """
        GSBase.start(self, pTargetInfoPolicy, pTargetSelectCallback, pTargetList)
        self.m_pTargetInfo.setRangePlotList(pTargetList)
        GerikSpellsInterface.showPlotSelectScreen(self, self.m_pTargetInfo)

        # Show the range plots if required.
        if (self.getShowRangeGraphics()):
            pCurrentSpell = GerikSpellsInterface.getCurrentSpell()
            for pPlot in self.m_pTargetInfo.getRangePlotList(pCurrentSpell.getCastingGroup(), pCurrentSpell.getCastingPlot()):
                self.drawRangePlot(pPlot)

        # Forces the first plots to be drawn.
        self.mouseOverPlot()

    # GERIKESTODO: What are the differences here?
    def stop(self, bCancel):
        """
        Arguments:
            None
        Returns
            None

        Undraws all plot drawings and stops doing plot targetting
        """
        GSBase.stop(self, bCancel)
        GerikSpellsInterface.getPlotSelectScreen().stop(bCancel)

    def mouseOverPlot(self):
        """
        Arguments:
            None
        Returns
            None

        Called every time the mouse scrolls over a new plot. Use CyInterface().getMouseOverPLot()
        to determine the new plot. This policy updates all the targets. 
        """
        self.updateTarget(CyInterface().getMouseOverPlot())
        self.draw()

    # Drawing functions
    def draw(self):
        """
        Arguments:
            None
        Returns
            None

        Draws all the targets to the screen.
        """
        self.undrawTargets()

        for pPlot in self.m_pCacheUnaffectedList:
            self.drawUnaffectedPlot(pPlot)
            
        for pPlot in self.m_pCacheAffectedList:
            self.drawAffectedPlot(pPlot)

    def drawAffectedPlot(self, pPlot):
        """
        Arguments:
            pPlot: The plot marked as "affected" to be drawn.
        Returns
            None

        Draws one affected plot.
        """
        CyEngine().addColoredPlotAlt(pPlot.getX(), pPlot.getY(), self.m_iAffectedPlotTexture, 2, self.m_szAffectedPlotColor, self.m_fAffectedPlotAlpha) # GERIKESTODO

    def drawUnaffectedPlot(self, pPlot):
        """
        Arguments:
            pPlot: The plot marked as "unaffected" to be drawn.
        Returns
            None

        Draws one unaffected plot.
        """
        CyEngine().addColoredPlotAlt(pPlot.getX(), pPlot.getY(), self.m_iUnaffectedPlotTexture, 2, self.m_szUnaffectedPlotColor, self.m_fUnaffectedPlotAlpha) # GERIKESTODO

    def drawRangePlot(self, pPlot):
        """
        Arguments:
            pPlot: The plot marked as "in range" to be drawn.
        Returns
            None

        Draws one in-range plot.
        """
        CyEngine().addColoredPlotAlt(pPlot.getX(), pPlot.getY(), self.m_iRangePlotTexture, 0, self.m_szRangePlotColor, self.m_fRangePlotAlpha) # GERIKESTODO
        
    def undraw(self):
        """
        Arguments:
            None
        Returns
            None

        Erases all affected, unaffected, and range plots by calling undrawRange() and undrawTargets().
        """
        self.undrawRange()
        self.undrawTargets()
        CyInterface().setDirty(InterfaceDirtyBits.ColoredPlots_DIRTY_BIT, True)

    def undrawRange(self):
        """
        Arguments:
            None
        Returns
            None

        Undraws only the ranged colored plots. 
        """
        CyEngine().clearColoredPlots(0) # GERIKESTODO

    def undrawTargets(self):
        """
        Arguments:
            None
        Returns
            None

        Undraws only the affected and unaffected colored plots.
        """
        CyEngine().clearColoredPlots(2) # GERIKESTODO

class GSPieMenu(GSBase):
    def __init__(self):
        """
        Arguments:
            None
        Returns
            None

        """
        GSBase.__init__(self)
        self.m_iFinalButtonDistanceFromCenter = 100
        self.m_iFinalButtonSize = 50
        self.m_pMakeListCallback = None
        self.m_iMaximumTargetsPerPage = 8
        self.m_szTurnPageButton = gc.getMissionInfo(gc.getInfoTypeForString("MISSION_SKIP")).getButton()

    def getButtonDistanceFromCenter(self):
        """
        Arguments:
            None
        Returns
            None

        Returns the size (in pixels) away from the middle of the pie menu each button should be.
        """
        return self.m_iFinalButtonDistanceFromCenter
    
    def setButtonDistanceFromCenter(self, iDistance):
        """
        Arguments:
            iDistance: The size (in pixels) away from the middle of the pie menu each button should be.
            None
        Returns
            None

        """
        self.m_iFinalButtonDistanceFromCenter = iDistance

    def getButtonSize(self):
        """
        Arguments:
            None
        Returns
            The size, in pixels, of the button's width and height.
        """
        return self.m_iFinalButtonSize

    def setButtonSize(self, iSize):
        """
        Arguments:
            iSize: The size, in pixels, of the button's width and height.
        Returns
            None
        """
        self.m_iFinalButtonSize = iSize

    def getMakePieMenuListCallback(self):
        """
        Arguments:
            None
        Returns
            The callback to the function that will construct the Pie Menu target list.
        """
        return self.m_pMakeListCallback

    def setMakePieMenuListCallback(self, pCallback):
        """
        Arguments:
            pCallback: Function name that should be called to make the Pie Menu target list.
        Returns
            None

        This function should be set by mods if they want to allow their own custom function to be called to place
        targets into the PieMenu target list. They can set this value to the name of one of their functions that will
        take in a list of TargetInfos. Then, that function should return the TargetInfos in a 2D list, with the first
        dimension being the pages, and the second dimensions storing lists of TargetInfos to show on that page.
        """

        self.m_pMakeListCallback = pCallback

    def getPieMenuMaximumTargetsPerPage(self):
        """
        Arguments:
            None
        Returns
            The maximum amount of Targets to show on one page in a pie menu target page. If the user has supplied their
            own function (using setMakePieMenuListCallback above), then his value is ignored. Otherwise, the policy
            will create for them a paged list of targets, filling each page up to the amount of targets specified in
            this value before starting a new page.
        """
        return self.m_iMaximumTargetsPerPage

    def setPieMenuMaximumTargetsPerPage(self, iMax):
        """
        Arguments:
            The maximum amount of targets to be shown on one page in a pie menu target page. See above.
        Returns
            None
        """
        self.m_iMaximumTargetsPerPage = iMax

    def getTurnPageButton(self):
        """
        Arguments:
            None
        Returns
            The art define button name for the "turn page" button.
        """
        return self.m_szTurnPageButton

    def setTurnPageButton(self, szTurnPageButton):
        """
        Arguments:
            szTurnPageButton: The art define button name for the "turn page" button.
        Returns
            None
        """
        self.m_szTurnPageButton = szTurnPageButton

    def start(self, pTargetInfoPolicy, pTargetSelectCallback, pTargetList):
        """
        Arguments:
            pTargetInfoPolicy: The TargetInfo policy class used to define what targets to show in the pie menu.
            pTargetSelectCallback: The function that will be called to return what target was selected.
            pTargetList: A list of TargetInfo classes with targets in them to show in the pie menu.
                         This list will be formatted into a 2D list for the pie menu during this function.
        Returns
            None

        Starts this graphics policy. This includes setting the interface mode (to ensure that the
        correct cursor is being used), creating the 2D target list from a list of targets, and finally
        starting the PieMenu screen.
        """
        GSBase.start(self, pTargetInfoPolicy, pTargetSelectCallback, pTargetList)
        
        # If the user wants to make the list themself, let them do it. Otherwise, default
        # to the standard "fill the pages to the max" algorithm.
        pCreator = self.getMakePieMenuListCallback()
        if (pCreator):
            pFinalTargetList = pCreator(pTargetList)
        else: 
            pFinalTargetList = GerikSpellsUtils.convertDataListIntoPages(pTargetList,self.getPieMenuMaximumTargetsPerPage())

        # Copy all the targets into an equal list, but with target infos rather than targets.
        pListTargetInfos = []
        for pPage in range(len(pFinalTargetList)):
            pListTargetInfos.append([])
            for pTarget in pFinalTargetList[pPage]:
                pTargetPolicyCopy = copy.copy(pTargetInfoPolicy)
                pTargetPolicyCopy.setTarget(pTarget)
                pListTargetInfos[pPage].append(pTargetPolicyCopy)
        

        GerikSpellsInterface.showPieMenuSelectScreen(self, pListTargetInfos)

    def stop(self, bCancel):
        """
        Arguments:
            None

        Returns:
            None

        Stops this graphics policy from running. This includes stopping the PieMenu screen. 
        """
        GSBase.stop(self, bCancel)
        GerikSpellsInterface.getPieMenuSelectScreen().stop(bCancel)

    def undraw(self):
        """
        Arguments:
            None

        Returns:
            None

        Not used by GSPieMenu policy class.
        """
        pass


    def mouseOverPlot(self):
        """
        Arguments:
            None
        Returns
            None

        Called whenever a new plot is highlighted. For this graphics policy, this does nothing.
        """

        return

    def getTargetButtonDimensions(self, iTargetNumber, iNumTargetsOnPage, fAnimationPercent):
        """
        Arguments:
            iTargetNumber: Number of the target on this page.
            iNumTargetsOnPage: The total number of targets on this page.
            fAnimationPercent: Gives the value of the pie menu's animation percent in a float value 0.0 - 100.0
        Returns
            4-Tuple:
                X-Offset (Distance from center horizontally to place the middle of this button)
                Y-Offset )Distance from center vertically to place the middle of this button)
                Width of Button
                Height of Button
                
        This is the guts of the drawing. This policy simply puts the targets in a round formation, equally
        spaced from the center, with no animation.
        """
        if (iNumTargetsOnPage == 0):
            CvUtil.pyAssert(False, "Recieved empty page in pie menu item list (divide by zero imminent)")
            return 
        # Angle of seperation between each button from the center (in radians)
        fAngle = (2 * math.pi / float(iNumTargetsOnPage))

        # Radians from the top (pi / 2), for use in determining where to place this button
        # Prevent round-off error.
        fRad = 0.0
        if (iTargetNumber == 0):
            fRad = -math.pi / 2.0
        else:
            fRad = (-math.pi / 2.0) + (float(iTargetNumber) * fAngle)

        return (
            int(self.m_iFinalButtonDistanceFromCenter * math.cos(fRad)), # X offset
            int(self.m_iFinalButtonDistanceFromCenter * math.sin(fRad)), # Y offset
            self.getButtonSize(),
            self.getButtonSize()
            )

    def getTurnPageButtonDimensions(self, fAnimationPercent):
        """
        Arguments:
            fAnimationPercent: The percent that the animation is complete in a float value 0.0 - 100.0
        Returns
            4-Tuple:
                X-Offset (Distance from center this button is horizontally)
                Y-Offset (Distance from center this button is vertically)
                Width of turn-page button.
                Height of turn-page button.
        """
        return (0, 0, self.m_iFinalButtonSize, self.m_iFinalButtonSize)

    def parseTargetInfo(self, iPageNum, iTargetNum, bStrategyOption):
        """
        Arguments:
            iPageNum: The page number of the target that we're trying to get the info of.
            iTargetNum: The target number of that page of the target we're trying to get the info of.
            bStrategyOption: True if the "Strategy Text" should be included.
        Returns
            String of the target's info text.

        This function is called to place info into the info pane box when a target is hovered over. By default,
        it calls the parseInfo() function of the TargetInfo policy.
        """
        return GerikSpellsInterface.getPieMenuSelectScreen().m_pTargetList[iPageNum][iTargetNum].parseInfo(bStrategyOption)

class GSPopup(GSBase):
    def __init__(self):
        """
        Arguments:
            None
        Returns
            None

        """
        GSBase.__init__(self)
        self.szTitleText = ""
        self.szCancelButtonText = "TXT_KEY_POPUP_CANCEL"
        self.bShowPictures = True
        self.pListTargetInfos = []
        self.pListCancelID = -1
    
    def getShowPictures(self):
        """
        Arguments:
            None
        Returns
            True if the GSPopup is set to show the pictures for each target.
            False if the GSPopup is set not to show the picture for each target.

        The picture for each target is defined within it's TargetInfo's "getButton" function.
        """
        return self.bShowPicture

    def setShowPictures(self, bShow):
        """
        Arguments:
            bShow: True or False, depending on if you want the picture to show for each target.
        Returns
            None

        The picture for each target is defined within it's TargetInfo's "getButton" function.
        """
        self.bShowPicture = bShow

    def getCancelButtonText(self):
        """
        Arguments:
            None
        Returns
            The text that will be shown in the "cancel" button.

        """
        return self.szCancelButtonText

    def setCancelButtonText(self, szText):
        """
        Arguments:
            szText: The text to show in the "cancel" button.
        Returns
            None
        """
        self.szCancelButtonText = szText

    def getTextTitle(self):
        """
        Arguments:
            None
        Returns
            The text that will be shown on the top of the popup.
        """
        return self.szTextTitle

    def setTitleText(self, szText):
        """
        Arguments:
            szText: The text to show on the top of the popup.
        Returns
            None
        """
        self.szTitleText = szText

    def undraw(self):
        """
        Arguments:
            None
        Returns
            None

        Does nothing yet. Will close the popup box when I figure out how to do that. But normally
        the popup closes by itself when you click a button.
        """
        # Gerikestodo: I don't think I can manually close a popup, but when I find out
        # how I need to put it here.
        pass

    def start(self, pTargetInfoPolicy, pTargetSelectCallback, pTargetList):
        """
        Arguments:
            pTargetInfoPolicy: A copy of the TargetInfo policy class to use for targets.
            pTargetSelectCallback: The function callback to use when a target is selected.
            pTargetList: A list of targets to be loaded into the popup.
        Returns
            None
        """
        GSBase.start(self, pTargetInfoPolicy, pTargetSelectCallback, pTargetList)
        self.pListTargetInfos = []
        self.pListCancelID = 0

        # Create the popup
        pPopup = CyPopupInfo()
        pPopup.setText(localtext.getText(self.szTitleText, ()))
        pPopup.setButtonPopupType(ButtonPopupTypes.BUTTONPOPUP_PYTHON)
        pPopup.setOnClickedPythonCallback("onClickGerikSpellsPopup")
        for pTarget in pTargetList:
            pTargetPolicyCopy = copy.copy(pTargetInfoPolicy)
            pTargetPolicyCopy.setTarget(pTarget)
            if (self.bShowPictures):
                pPopup.addPythonButton(pTargetPolicyCopy.parseInfo(False), pTargetPolicyCopy.getButton())
            else:
                pPopup.addPythonButton(pTargetPolicyCopy.parseInfo(False), "")
                
                
            self.pListTargetInfos.append(pTargetPolicyCopy)
            self.pListCancelID += 1
        
        # Create the "cancel" button.
        pPopup.addPythonButton(localtext.getText(self.szCancelButtonText, ()), "")

        # Show the popup. Since this should only be run on the computer where the player has the spell running, using
        # getActivePlayer should not cause problems.
        pPopup.addPopup(gc.getGame().getActivePlayer())

    def stop(self, bCancel):
        """
        Arguments:
            bCancel: If the reason to close is because the spell's activation has been cancelled.
        Returns
            None
        """
        GSBase.stop(self, bCancel)
        if bCancel:
            GerikSpellsInterface.resetLocalInterfaceMode()

    def handlePopupClick(self, pPopupReturn, iData1, iData2, iData3, iFlags, szText, bOption1, bOption2):
        """
        Arguments:
            Arguments for a typical popup return.
        Returns
            None

        If the button pressed was cancel, cancels the spell.
        If the button pressed was a target, tells the handler walk target was selected.
        """
        if (pPopupReturn == self.pListCancelID):
            GerikSpellsInterface.cancelSpell()
        else:
            self.targetSelected(self.pListTargetInfos[pPopupReturn])

